/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package erwiki.plugins;

import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.log4j.Logger;
import org.elwiki.IWikiConstants;
import org.elwiki.permissions.PagePermission;
import org.elwiki_data.IHistoryInfo;
import org.elwiki_data.PageContent;
import org.elwiki_data.WikiPage;
import org.osgi.framework.FrameworkUtil;

import erwiki.api.WikiPartReference;
import erwiki.api.auth.AuthorizationManager;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiContext;
import erwiki.api.core.WikiContext.TimeFormat;
import erwiki.api.exceptions.PluginException;
import erwiki.api.pages.Id2NamePage;
import erwiki.api.pages.PageManager;
import erwiki.api.plugin.IWeblogPlugin;
import erwiki.api.plugin.ParserStagePlugin;
import erwiki.api.plugin.PluginElement;
import erwiki.api.plugin.PluginManager;
import erwiki.api.plugin.WikiPlugin;
import erwiki.api.preferences.Preferences;
import erwiki.api.providers.WikiProvider;
import erwiki.api.render.RenderingManager;
import erwiki.api.rwc.RWC;
import erwiki.util.TextUtil;

/**
 * Плагин ErWiki. Создает простой блог. Формат страницы может использовать следующие параметры:
 * <ul>
 * <li>%p - имя страницы.</li>
 * </ul>
 * <p>
 * Параметры плагина:
 * <ul>
 * <li><b>page</b> - указывает страницу, используемую для ведения блога; по умолчанию – текущая
 * страница.</li>
 * <li><b>entryFormat</b> - определяет формат отображения даты на страницах, используя синтаксис
 * J2SE SimpleDateFormat. По умолчанию используется формат DateFormat.LONG текущей локали для даты.
 * Таким образом, для локали RU будут напечатаны даты, похожие на следующее: 4 сентября 2005 г.</li>
 * <li><b>days</b> - сколько дней должен выводить агрегатор блогов. Если установлено "all", то
 * отображаются все страницы.</li>
 * <li><b>pageformat</b> - Определяет вид титульных страниц.</li>
 * <li><b>startDate</b> - Дата, когда следует начать. Формат – "ddMMyy".</li>
 * <li><b>maxEntries</b> - Максимальное количество отображаемых записей.</li>
 * <li><b>preview</b> - Число символов текста, выводимых на странице предварительного
 * просмотра.</li>
 * </ul>
 * <p>
 * Значения "days" и "startDate" также могут быть отправлены в параметрах HTTP, их имена –
 * "weblog.days" и "weblog.startDate" соответственно.
 * <p>
 * Плагин веб-журнала также добавляет атрибут к каждой странице, на которой он находится: для
 * параметра "weblogplugin.isweblog" устанавливается значение "true". Это можно использовать для
 * быстрого просмотра страниц с блогами.
 */

// FIXME: Add "entries" param as an alternative to "days".
// FIXME: Entries arrive in wrong order.

public class WeblogPlugin implements WikiPlugin, IWeblogPlugin, ParserStagePlugin {

	private static final Logger log = Logger.getLogger(WeblogPlugin.class);

	private static final Pattern HEADINGPATTERN;

	/** Сколько дней считается по умолчанию. Значение по умолчанию: {@value}. */
	private static final int DEFAULT_DAYS = 7;

	private static final String DEFAULT_PAGEFORMAT = "%p_blogentry_";

	/** Формат даты по умолчанию, используемый в названиях страниц записей блога. */
	public static final String DEFAULT_DATEFORMAT = "ddMMyy";

	/** Имя параметра для <i>startDate</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_STARTDATE = "startDate";

	/** Имя параметра для <i>entryFormat</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_ENTRYFORMAT = "entryFormat";

	/** Имя параметра для <i>days</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_DAYS = "days";

	/** Имя параметра для <i>allowComments</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_ALLOWCOMMENTS = "allowComments";

	/** Имя параметра для <i>maxEntries</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_MAXENTRIES = "maxEntries";

	/** Имя параметра для <i>page</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_PAGE = "page";

	/** Имя параметра для <i>preview</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_PREVIEW = "preview";

	/**
	 * Атрибут, который размещается в атрибутах WikiPage и позволяет проверить, является ли страница
	 * веб-журналом или нет.</br>
	 * Его наличие можно проверить.
	 */
	public static final String ATTR_ISWEBLOG = "weblogplugin.isweblog";

	static {
		// Это довольно уродливое регулярное выражение перебора. Но пока сойдет...
		HEADINGPATTERN = Pattern.compile("(<h[1-4][^>]*>)(.*)(</h[1-4]>)", Pattern.CASE_INSENSITIVE);
	}

	@WikiPartReference
	private Engine engine;

	@WikiPartReference
	private AuthorizationManager authorizationManager;

	@WikiPartReference
	private PageManager pageManager;

	@WikiPartReference
	private RenderingManager renderingManager;

	@WikiPartReference
	private Id2NamePage id2NamePage;

	/**
	 * Создает имя записи на основе имени блога, даты и номера записи.
	 *
	 * @param pageName Название блога.
	 * @param date     Дата (в формате ddMMyy).
	 * @param entryNum Номер записи.
	 * @return Отформатированное имя страницы.
	 */
	public static String makeEntryPage(String pageName, String date, String entryNum) {
		return TextUtil.replaceString(DEFAULT_PAGEFORMAT, "%p", pageName) + date + "_" + entryNum;
	}

	/**
	 * Возвращает только базовое имя для записей, без даты и номера записи.
	 *
	 * @param pageName Название блога.
	 * @return Отформатированное имя.
	 */
	public static String makeEntryPage(String pageName) {
		return TextUtil.replaceString(DEFAULT_PAGEFORMAT, "%p", pageName);
	}

	/**
	 * Возвращает страницу записи без номера записи.
	 *
	 * @param pageName Название блога.
	 * @param date     Дата.
	 * @return Базовое имя для записей блога.
	 */
	public static String makeEntryPage(String pageName, String date) {
		return TextUtil.replaceString(DEFAULT_PAGEFORMAT, "%p", pageName) + date;
	}

	/** {@inheritDoc} */
	@Override
	public String execute(WikiContext context, Map<String, String> params) throws PluginException {
		Calendar startTime;
		Calendar stopTime;
		int numDays = DEFAULT_DAYS;

		/* Разбор параметров.
		 */
		String days;
		DateFormat entryFormat;
		String startDay;
		boolean hasComments = false;
		int maxEntries;
		String weblogName;

		if ((weblogName = params.get(PARAM_PAGE)) == null) {
			weblogName = context.getPage().getName();
		}

		if ((days = context.getHttpParameter("weblog." + PARAM_DAYS)) == null) {
			days = params.get(PARAM_DAYS);
		}

		if ((params.get(PARAM_ENTRYFORMAT)) == null) {
			entryFormat = Preferences.getDateFormat(context, TimeFormat.DATETIME);
		} else {
			entryFormat = new SimpleDateFormat(params.get(PARAM_ENTRYFORMAT));
		}

		if (days != null) {
			if (days.equalsIgnoreCase("all")) {
				numDays = Integer.MAX_VALUE;
			} else {
				numDays = NumberUtils.toInt(days, DEFAULT_DAYS);
			}
		}

		if ((startDay = params.get(PARAM_STARTDATE)) == null) {
			startDay = context.getHttpParameter("weblog." + PARAM_STARTDATE);
		}

		var value = BooleanUtils.toBooleanObject(params.get(PARAM_ALLOWCOMMENTS));
		if (value != null && value) {
			hasComments = true;
		}

		maxEntries = NumberUtils.toInt(params.get(PARAM_MAXENTRIES), Integer.MAX_VALUE);

		/* Определение диапазона дат, которые следует включить.
		 */
		startTime = Calendar.getInstance();
		stopTime = Calendar.getInstance();

		if (startDay != null) {
			SimpleDateFormat fmt = new SimpleDateFormat(DEFAULT_DATEFORMAT);
			try {
				Date d = fmt.parse(startDay);
				startTime.setTime(d);
				stopTime.setTime(d);
			} catch (ParseException e) {
				return "Illegal time format: " + startDay;
			}
		}

		/* Отметить это как блог.
		 */
		context.getPage().setAttribute(ATTR_ISWEBLOG, "true");

		//
		// Мы делаем дикое предположение, что здесь никто не может обеспечить точность до миллисекунды.
		//
		startTime.add(Calendar.DAY_OF_MONTH, -numDays);
		startTime.set(Calendar.HOUR, 0);
		startTime.set(Calendar.MINUTE, 0);
		startTime.set(Calendar.SECOND, 0);
		stopTime.set(Calendar.HOUR, 23);
		stopTime.set(Calendar.MINUTE, 59);
		stopTime.set(Calendar.SECOND, 59);

		List<PageContent> blogEntries = findBlogEntries(weblogName, startTime.getTime(), stopTime.getTime());
		// Обратная сортировка по дате.
		Comparator<IHistoryInfo> histInfoComparator = Comparator.comparing(IHistoryInfo::getCreationDate).reversed();
		Collections.sort(blogEntries, histInfoComparator);

		StringBuilder sb = new StringBuilder();
		sb.append("<div class=\"weblog\">\n");

		for (PageContent pc : blogEntries) {
			if (maxEntries-- > 0) {
				WikiPage p = pc.getWikipage();
				if (authorizationManager.checkPermission(context.getWikiSession(),
						new PagePermission(p, PagePermission.VIEW_ACTION))) {
					addEntryHTML(context, entryFormat, hasComments, sb, pc, params);
				}
			}
		}

		sb.append("</div>\n");

		return sb.toString();
	}

	/**
	 * Генерирует HTML для требуемой записи в блоге.
	 *
	 * @param context
	 * @param entryFormat
	 * @param hasComments <code>True</code>, если комментарии включены.
	 * @param buffer      Буфер, в который добавляется результат.
	 * @param pageContent
	 * @throws PluginException TODO
	 */
	private void addEntryHTML(WikiContext context, DateFormat entryFormat, boolean hasComments, StringBuilder buffer,
			PageContent pageContent, Map<String, String> params) throws PluginException {
		WikiPage page = pageContent.getWikipage();

		PluginManager pluginManager = context.getEngine().getManager(PluginManager.class);
		ResourceBundle rb = pluginManager.getBundle( //
				Preferences.getLocale(), FrameworkUtil.getBundle(this.getClass()));

		buffer.append("<div class=\"weblogentry\">\n");

		/* Заголовок.
		 */
		buffer.append("<div class=\"weblogentryheading\">\n");

		Date entryDate = pageContent.getCreationDate();
		buffer.append(entryFormat != null ? entryFormat.format(entryDate) : entryDate);
		buffer.append("</div>\n");

		/* Добавление текста последней версии. Сброс содержимого этой страницы.
		 */
		//:FVK: ... WikiContext entryCtx = null; //TODO::FVK: context.clone();
		//entryCtx.setPage(page);
		String html = this.renderingManager.getHTML(this.pageManager.getPageByName(page.getName()));

		// Извлечь первый h1/h2/h3, как заголовок. И заменить его на "".
		buffer.append("<div class=\"weblogentrytitle\">\n");
		Matcher matcher = HEADINGPATTERN.matcher(html);
		if (matcher.find()) {
			String title = matcher.group(2);
			html = matcher.replaceFirst("");
			buffer.append(title);
		} else {
			buffer.append(page.getName());
		}
		buffer.append("</div>\n");
		buffer.append("<div class=\"weblogentrybody\">\n");

		int preview = NumberUtils.toInt(params.get(PARAM_PREVIEW), 0);
		if (preview > 0) {
			/* Начнем с первого «предпросмотрового» количества символов текста,
			 * а затем добавляем к нему символы, пока не дойдем до разрыва строки или точки.
			 * Идея в том, что обрезание при разрыве строки менее вероятно,
			 * чтобы нарушить HTML и оставить нам искаженный вывод.
			 */
			boolean hasBeenCutOff = false;
			int cutoff = Math.min(preview, html.length());
			while (cutoff < html.length()) {
				if (html.charAt(cutoff) == '\r' || html.charAt(cutoff) == '\n') {
					hasBeenCutOff = true;
					break;
				} else if (html.charAt(cutoff) == '.') {
					// ожидается точка
					cutoff++;
					hasBeenCutOff = true;
					break;
				}
				cutoff++;
			}
			buffer.append(html.substring(0, cutoff));
			if (hasBeenCutOff) {
				buffer.append(" <a href=\""//
						+ engine.getURL(WikiContext.PAGE_VIEW, page.getName(), null) //
						+ "\">" + rb.getString("weblogentryplugin.more") + "</a>\n");
			}
		} else {
			buffer.append(html);
		}
		buffer.append("</div>\n");

		/* Добавление нижнего колонтитула.
		 */
		buffer.append("<div class=\"weblogentryfooter\">\n");

		String author = pageContent.getAuthor();

		if (author != null) {
			if (this.pageManager.pageExistsByName(author)) {
				author = "<a href=\"" + engine.getURL(WikiContext.PAGE_VIEW, author,null) + "\">" + author + "</a>";
			}
		} else {
			author = "AnonymousCoward";
		}

		buffer.append(MessageFormat.format(rb.getString("weblogentryplugin.postedby"), author));
		buffer.append("<a href=\"" + engine.getURL(WikiContext.PAGE_VIEW, page.getName(),null) + "\">"
				+ rb.getString("weblogentryplugin.permalink") + "</a>");
		String commentPageName = TextUtil.replaceString(page.getName(), "blogentry", "comments");

		if (hasComments) {
			int numComments = guessNumberOfComments(commentPageName);

			/* Добавление нескольких комментариев к URL,
			 * чтобы браузеры пользователя понимали, что страница изменилась.
			 */
			buffer.append("&nbsp;&nbsp;");

			String addcomment = rb.getString("weblogentryplugin.addcomment");

			buffer.append("<a href=\"" + engine.getURL(WikiContext.PAGE_COMMENT, commentPageName, "nc=" + numComments)
					+ "\">" + MessageFormat.format(addcomment, numComments) + "</a>");
		}

		buffer.append("</div>\n");

		// Готово, закрыть.
		buffer.append("</div>\n");
	}

	private int guessNumberOfComments(String commentpage) {
		String pagedata = this.pageManager.getPureText(commentpage, IWikiConstants.LATEST_VERSION);
		if (pagedata == null || pagedata.trim().length() == 0) {
			return 0;
		}

		return TextUtil.countSections(pagedata);
	}

	/**
	 * Пытается найти все страницы, соответствующие шаблону записи блога.</br>
	 * В датах рассматриваются только дни, часы и минуты не учитываются.</br>
	 * <b>Замечание:</b> у страницы анализируется дата последней версии страницы.
	 *
	 * @param baseName Базовое имя (например, "Main", если требуется "Main_blogentry_xxxx").
	 * @param start    Начальная дата интервала рассматриваемых страниц.
	 * @param end      Конечная дата интервала рассматриваемых страниц.
	 *
	 * @return Список содержимого страниц блога, за указанный период.
	 */
	public List<PageContent> findBlogEntries(String baseName, Date start, Date end) {
		String[] allPages = this.id2NamePage.getAllPageNames();
		ArrayList<PageContent> result = new ArrayList<>();

		baseName = makeEntryPage(baseName);

		for (String pageName : allPages) {
			if (pageName.startsWith(baseName)) {
				try {
					WikiPage wikiPage = this.pageManager.getPageByName(baseName);
					PageContent pc = wikiPage.getLastContent();
					Date pageCreationDate;
					if (pc != null && (pageCreationDate = pc.getCreationDate()) != null) {
						if (pageCreationDate.after(start) && pageCreationDate.before(end)) {
							result.add(pc);
						}
					}
				} catch (Exception e) {
					log.debug("Page name \"" + pageName + "\" was suspected as a blog entry but something went wrong.",
							e);
				}
			}
		}

		return result;
	}

	/**
	 * Производит пометку страницы, как блога.
	 *
	 * {@inheritDoc}
	 */
	@Override
	public void executeParser(PluginElement element, Map<String, String> params) {
		RWC.INSTANCE.getPage().setAttribute(ATTR_ISWEBLOG, "true");
	}

}
